import math
import random

def sigmoid(x):
    return math.tanh(x)

def dsigmoid(y):
    return 1.0 - y**2

class NeuralNetwork:
    def __init__(self, input_nodes, hidden_nodes, output_nodes):
        self.input_nodes = input_nodes + 1
        self.hidden_nodes = hidden_nodes
        self.output_nodes = output_nodes
        
        self.ai = [1.0] * self.input_nodes
        self.ah = [1.0] * self.hidden_nodes
        self.ao = [1.0] * self.output_nodes

        self.wi = [[random.uniform(-0.2, 0.2) for j in range(self.input_nodes)] 
                   for i in range(self.hidden_nodes)]
        self.wo = [[random.uniform(-2.0, 2.0) for j in range(self.hidden_nodes)]
                   for i in range(self.output_nodes)]
        
        self.ci = [[0.0] * self.input_nodes for i in range(self.hidden_nodes)]
        self.co = [[0.0] * self.hidden_nodes for i in range(self.output_nodes)]

    def update(self, inputs):
        if len(inputs) != self.input_nodes - 1:
            raise ValueError('wrong number of inputs')
        
        for i in range(self.input_nodes - 1):
            self.ai[i] = inputs[i]

        for j in range(self.hidden_nodes):
            sum = 0.0
            for i in range(self.input_nodes):
                sum += self.ai[i] * self.wi[j][i]
            self.ah[j] = sigmoid(sum)

        for k in range(self.output_nodes):
            sum = 0.0
            for j in range(self.hidden_nodes):
                sum += self.ah[j] * self.wo[k][j]
            self.ao[k] = sigmoid(sum)

        return self.ao[:]

    def back_propagate(self, targets, N, M):
        if len(targets) != self.output_nodes:
            raise ValueError('wrong number of target values')
        
        output_deltas = [0.0] * self.output_nodes
        for k in range(self.output_nodes):
            error = targets[k] - self.ao[k]
            output_deltas[k] = dsigmoid(self.ao[k]) * error

        hidden_deltas = [0.0] * self.hidden_nodes
        for j in range(self.hidden_nodes):
            error = 0.0
            for k in range(self.output_nodes):
                error += output_deltas[k] * self.wo[k][j]
            hidden_deltas[j] = dsigmoid(self.ah[j]) * error

        for j in range(self.hidden_nodes):
            for k in range(self.output_nodes):
                change = output_deltas[k] * self.ah[j]
                self.wo[k][j] += N * change + M * self.co[k][j]
                self.co[k][j] = change

        for i in range(self.input_nodes):
            for j in range(self.hidden_nodes):
                change = hidden_deltas[j] * self.ai[i]
                self.wi[j][i] += N * change + M * self.ci[j][i]
                self.ci[j][i] = change

        error = 0.0
        for k in range(len(targets)):
            error += 0.5 * (targets[k] - self.ao[k]) ** 2
        return error
    def test(self, patterns):
        for p in patterns:
            print(p[0], '->', self.update(p[0]))

    def weights(self):
        print('input->hidden')
        for i in range(self.input_nodes):
            print(self.wi[i])
        print()
        print('hidden->output')
        for j in range(self.hidden_nodes):
            print(self.wo[j])

    def train(self, patterns, iterations=1000, N=0.5, M=0.1):
        for i in range(iterations):
            error = 0.0
            for p in patterns:
                inputs = p[0]
                targets = p[1]
                self.update(inputs)
                error += self.back_propagate(targets, N, M)
            if i % 100 == 0:
                print('error %-.5f' % error)

def demo():
    n = NeuralNetwork(2, 2, 1)
    patterns = [
        [[0, 0], [0]],
        [[0, 1], [1]],
        [[1, 0], [1]],
        [[1, 1], [0]]
    ]
    n.train(patterns)
    n.test(patterns)

if __name__ == '__main__':
    demo()